package com.qen.truelicense.license;

import com.qen.truelicense.util.ObfuscatedString;
import com.qen.truelicense.xml.GenericCertificate;

import javax.security.auth.x500.X500Principal;
import javax.swing.filechooser.FileFilter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Calendar;
import java.util.Date;
import java.util.prefs.Preferences;

/**
 * This is the top level class which manages all licensing aspects like for
 * instance the creation, installation and verification of license keys.
 * The license manager knows how to install, verify and uninstall full and
 * trial licenses for a given subject and ensures the privacy of the license
 * content in its persistent form (i.e. the <i>license key</i>).
 * For signing, verifying and validating licenses, this class cooperates with
 * a {@link LicenseNotary}.
 * <p>
 * This class is thread-safe.
 *
 * @author licz
 */
public class LicenseManager implements LicenseCreator, LicenseVerifier {

    /**
     * The timeout for the license content cache.
     */
    private static final long TIMEOUT = 30 * 60 * 1000; // half an hour

    /**
     * The key in the preferences used to store the license key.
     * <p>
     * => "license"
     */
    private static final String PREFERENCES_KEY = new ObfuscatedString(new long[] {0x64416B54D0E5A4F4L, 0xC9032DBC6E1B5698L}).toString();

    /**
     * The suffix for files which hold license certificates.
     * <p>
     * => ".lic" - must be lowercase!
     */
    public static final String LICENSE_SUFFIX = new ObfuscatedString(new long[] {0xA1A6D2617E5B36A9L, 0xA1900083616CAB14L}).toString();

    static {
        assert LICENSE_SUFFIX.equals(LICENSE_SUFFIX.toLowerCase()); // paranoid
    }

    /**
     * => "CN="
     */
    protected static final String CN = new ObfuscatedString(new long[] {0x7C9F3989FBD42308L, 0xD09969A067D8544FL}).toString();

    /**
     * => localized string for resource key "user"
     */
    private static final String CN_USER = CN + Resources.getString(new ObfuscatedString(new long[] {0xCF9F7A0272792C0FL, 0xCDD49DF334B87730L}).toString());

    /**
     * => "User"
     */
    private static final String USER = new ObfuscatedString(new long[] {0xB86813DA7F50EB11L, 0xD200C77F74AEF203L}).toString();

    /**
     * => "System"
     */
    private static final String SYSTEM = new ObfuscatedString(new long[] {0x1AC961D762D5E343L, 0xCCA643333A33257L}).toString();

    /**
     * => "exc.invalidSubject"
     */
    private static final String EXC_INVALID_SUBJECT = new ObfuscatedString(new long[] {0xBCBF77B393E5C6E0L, 0x324C2675B9949974L, 0x7DC9455AFD687EDCL, 0x1D793B67B8F692DAL}).toString();

    /**
     * => "exc.holderIsNull"
     */
    private static final String EXC_HOLDER_IS_NULL = new ObfuscatedString(new long[] {0x1851BBE12A6E16CDL, 0x38C63C079763CF2DL, 0xAAEC8E15D62C7127L}).toString();

    /**
     * => "exc.issuerIsNull"
     */
    private static final String EXC_ISSUER_IS_NULL = new ObfuscatedString(new long[] {0x70424E79340BFFCL, 0xA3F449EF4AB5CDDBL, 0x8778B52BEF262F3AL}).toString();

    /**
     * => "exc.issuedIsNull"
     */
    private static final String EXC_ISSUED_IS_NULL = new ObfuscatedString(new long[] {0xF034009D3B1EEC93L, 0xC222B77CA2526847L, 0x7F5E598BC3DDE4F6L}).toString();

    /**
     * => "exc.licenseIsNotYetValid"
     */
    private static final String EXC_LICENSE_IS_NOT_YET_VALID = new ObfuscatedString(new long[] {0x6C30E15BA56503D6L, 0xA854191394113CB3L, 0xEA7E068EBB184E3FL, 0x4AAC99D676ED5BE1L}).toString();

    /**
     * => "exc.licenseHasExpired"
     */
    private static final String EXC_LICENSE_HAS_EXPIRED = new ObfuscatedString(new long[] {0xA0D9E521CB41A3CDL, 0xF370BFBFC0B7A092L, 0x31B55A3E7FCBFBF2L, 0x6D43E9A9E62EA08CL}).toString();

    /**
     * => "exc.consumerTypeIsNull"
     */
    private static final String EXC_CONSUMER_TYPE_IS_NULL = new ObfuscatedString(new long[] {0xD9F1A70F65CA2BAEL, 0x85D43946A1F25824L, 0x9CCD25B11ED12742L, 0x3C8C3784D7BC28B2L}).toString();

    /**
     * => "exc.consumerTypeIsNotUser"
     */
    private static final String EXC_CONSUMER_TYPE_IS_NOT_USER = new ObfuscatedString(new long[] {0x82BD0FFE363B7B0BL, 0xA6DB065C51914766L, 0xF49046A12530872L, 0xC2D4113B300C2DAAL, 0x177560BA766D4C94L}).toString();

    /**
     * => "exc.consumerAmountIsNotOne"
     */
    private static final String EXC_CONSUMER_AMOUNT_IS_NOT_ONE = new ObfuscatedString(new long[] {0x18EC1C768834CD1EL, 0x683896740593B3CFL, 0xE10B8A7D2633076L, 0x1D8FB4D1C18E72A3L, 0x7B0F9424621B0B7DL}).toString();

    /**
     * => "exc.consumerAmountIsNotPositive"
     */
    private static final String EXC_CONSUMER_AMOUNT_IS_NOT_POSITIVE = new ObfuscatedString(new long[] {0xD4BBB02913333178L, 0x2D3C2F85FD145726L, 0x3999414E0837867BL, 0xDEA80EF0B7A6F902L, 0x5FBF44D5915F857BL}).toString();

    /**
     * => "fileFilter.description"
     */
    private static final String FILE_FILTER_DESCRIPTION = new ObfuscatedString(new long[] {0x416EC7E47B1F6493L, 0xF95B1FC04C18B60AL, 0xF02ACECAA69C1E51L, 0x7D5E5E91BB70D24DL}).toString();

    /**
     * => " (*.lic)"
     */
    private static final String FILE_FILTER_SUFFIX = new ObfuscatedString(new long[] {0x161E1B7F025FFC4DL, 0x5DD5D4530BE24E80L}).toString();

    /**
     * Returns midnight local time today.
     */
    protected static Date midnight() {
        Calendar cal = Calendar.getInstance();
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);

        return cal.getTime();
    }

    private LicenseParam param; // initialized by setLicenseParam() - should be accessed via getLicenseParam() only!

    //
    // Data computed and cached from the license configuration parameters.
    //

    private LicenseNotary notary; // lazy initialized

    private PrivacyGuard guard; // lazy initialized

    /**
     * The cached certificate of the current license key.
     */
    private GenericCertificate certificate; // lazy initialized

    /**
     * The time when the certificate was last set.
     */
    private long certificateTimeout; // lazy initialized

    /**
     * A suitable file filter for the subject of this license manager.
     */
    private FileFilter fileFilter; // lazy initialized

    /**
     * Constructs a License Manager.
     * <p>
     * <b>Warning:</b> The manager created by this constructor is <em>not</em>
     * valid and cannot be used unless {@link #setLicenseParam(LicenseParam)}
     * is called!
     */
    protected LicenseManager() {
    }

    /**
     * Constructs a License Manager.
     *
     * @param param the license configuration parameters
     *              - may <em>not</em> be {@code null}.
     * @throws NullPointerException     If the given parameter object does not
     *                                  obey the contract of its interface due to a {@code null}
     *                                  pointer.
     * @throws IllegalPasswordException If any password in the parameter object
     *                                  does not comply to the current policy.
     */
    public LicenseManager(LicenseParam param) {
        setLicenseParam0(param);
    }

    /**
     * Returns the license configuration parameters.
     */
    public synchronized LicenseParam getLicenseParam() {
        return param;
    }

    /**
     * Sets the license configuration parameters.
     * Calling this method resets the manager as if it had been
     * newly created.
     * Some plausibility checks are applied to the given parameter object
     * to ensure that it adheres to the contract of the parameter interfaces.
     *
     * @param param the license configuration parameters
     *              - may <em>not</em> be {@code null}.
     * @throws NullPointerException     If the given parameter object does not
     *                                  obey the contract of its interface due to a {@code null}
     *                                  pointer.
     * @throws IllegalPasswordException If any password in the parameter object
     *                                  does not comply to the current policy.
     */
    public synchronized void setLicenseParam(LicenseParam param) {
        setLicenseParam0(param);
    }

    private void setLicenseParam0(LicenseParam param) {
        // Check parameters to implement fail-fast behaviour.
        final CipherParam cipherParam;
        if (null == param.getSubject() || null == param.getKeyStoreParam() || null == (cipherParam = param.getCipherParam())) {
            throw new NullPointerException();
        }
        // DONT DO THIS - ONLY REQUIRED FOR INSTALLATION OR VERIFICATION!
        //if (null == param.getPreferences())
        //    throw new NullPointerException();
        Policy.getCurrent().checkPwd(cipherParam.getKeyPwd());

        this.param = param;
        notary = null;
        setCertificate0(null);
        fileFilter = null;
    }

    //
    // Methods for license contents.
    //

    /**
     * Initializes and validates the license content, creates a new signed
     * license certificate for it and compresses, encrypts and stores it to
     * the given file as a license key.
     * <p>
     * As a side effect, the given license {@code content} is initialized
     * with some reasonable defaults unless the respective properties have
     * already been set.
     *
     * @param content the license content
     *                - may <em>not</em> be {@code null}.
     * @param keyFile the file to save the license key to
     *                - may <em>not</em> be {@code null}.
     *                This should have a {@code LICENSE_SUFFIX}.
     * @throws Exception for various reasons. Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #store(LicenseContent, LicenseNotary, File)
     * @see #initialize(LicenseContent)
     * @see #validate(LicenseContent)
     */
    public final synchronized void store(LicenseContent content, File keyFile) throws Exception {
        store(content, getLicenseNotary(), keyFile);
    }

    /**
     * Initializes and validates the license content, creates a new signed
     * license certificate for it and compresses, encrypts and stores it to
     * the given file as a license key.
     * <p>
     * As a side effect, the given license {@code content} is initialized
     * with some reasonable defaults unless the respective properties have
     * already been set.
     *
     * @param content the license content
     *                - may <em>not</em> be {@code null}.
     * @param notary  the license notary used to sign the license key
     *                - may <em>not</em> be {@code null}.
     * @param keyFile the file to save the license key to
     *                - may <em>not</em> be {@code null}.
     *                This should have a {@code LICENSE_SUFFIX}.
     * @throws Exception for various reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #initialize(LicenseContent)
     * @see #validate(LicenseContent)
     */
    protected synchronized void store(LicenseContent content, LicenseNotary notary, File keyFile) throws Exception {
        storeLicenseKey(create(content, notary), keyFile);
    }

    /**
     * Initializes and validates the license content, creates a new signed
     * license certificate for it and compresses, encrypts and returns it
     * as a license key.
     * <p>
     * As a side effect, the given license {@code content} is initialized
     * with some reasonable defaults unless the respective properties have
     * already been set.
     *
     * @param content the license content
     *                - may <em>not</em> be {@code null}.
     * @return The license key
     * - {@code null} is never returned.
     * @throws Exception for various reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #create(LicenseContent, LicenseNotary)
     * @see #initialize(LicenseContent)
     * @see #validate(LicenseContent)
     */
    public final synchronized byte[] create(LicenseContent content) throws Exception {
        return create(content, getLicenseNotary());
    }

    /**
     * Initializes and validates the license content, creates a new signed
     * license certificate for it and compresses, encrypts and returns it
     * as a license key.
     * <p>
     * As a side effect, the given license {@code content} is initialized
     * with some reasonable defaults unless the respective properties have
     * already been set.
     *
     * @param content the license content
     *                - may <em>not</em> be {@code null}.
     * @param notary  the license notary used to sign the license key
     *                - may <em>not</em> be {@code null}.
     * @return The license key
     * - {@code null} is never returned.
     * @throws Exception for various reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #initialize(LicenseContent)
     * @see #validate(LicenseContent)
     */
    protected synchronized byte[] create(LicenseContent content, LicenseNotary notary) throws Exception {
        initialize(content);
        validate(content);
        final GenericCertificate certificate = notary.sign(content);
        return getPrivacyGuard().cert2key(certificate);
    }

    /**
     * Loads, decrypts, decompresses, decodes and verifies the license key in
     * {@code keyFile}, validates its license content and installs it
     * as the current license key.
     *
     * @param keyFile the file to load the license key from
     *                - may <em>not</em> be {@code null}.
     * @return A clone of the verified and validated content of the license key
     * - {@code null} is never returned.
     * @throws Exception for various reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #install(File, LicenseNotary)
     * @see #validate(LicenseContent)
     */
    public final synchronized LicenseContent install(File keyFile) throws Exception {
        return install(keyFile, getLicenseNotary());
    }

    /**
     * Loads, decrypts, decompresses, decodes and verifies the license key in
     * {@code keyFile}, validates its license content and installs it
     * as the current license key.
     *
     * @param keyFile The file to load the license key from
     *                - may <em>not</em> be {@code null}.
     * @param notary  The license notary used to verify the license key
     *                - may <em>not</em> be {@code null}.
     * @return A clone of the verified and validated content of the license key
     * - {@code null} is never returned.
     * @throws Exception for various reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #validate(LicenseContent)
     */
    protected synchronized LicenseContent install(File keyFile, LicenseNotary notary) throws Exception {
        return install(loadLicenseKey(keyFile), notary);
    }

    /**
     * Decrypts, decompresses, decodes and verifies the license key in
     * {@code key}, validates its license content and installs it
     * as the current license key.
     *
     * @param key    the license key
     *               - may <em>not</em> be {@code null}.
     * @param notary the license notary used to verify the license key
     *               - may <em>not</em> be {@code null}.
     * @return A clone of the verified and validated content of the license key
     * - {@code null} is never returned.
     * @throws Exception for various reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #validate(LicenseContent)
     */
    protected synchronized LicenseContent install(final byte[] key, final LicenseNotary notary) throws Exception {
        final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
        notary.verify(certificate);
        final LicenseContent content = (LicenseContent) certificate.getContent();
        validate(content);
        setLicenseKey(key);
        setCertificate(certificate);

        return content;
    }

    /**
     * Decrypts, decompresses, decodes and verifies the current license key,
     * validates its license content and returns it.
     *
     * @return A clone of the verified and validated content of the license key
     * - {@code null} is never returned.
     * @throws NoLicenseInstalledException if no license key is installed.
     * @throws Exception                   for any other reason.
     *                                     Note that you should always use
     *                                     {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                                     localized) meaningful detail message.
     * @see #validate(LicenseContent)
     */
    public final synchronized LicenseContent verify() throws Exception {
        return verify(getLicenseNotary());
    }

    /**
     * Decrypts, decompresses, decodes and verifies the current license key,
     * validates its license content and returns it.
     *
     * @param notary the license notary used to verify the current license key
     *               - may <em>not</em> be {@code null}.
     * @return A clone of the verified and validated content of the license key
     * - {@code null} is never returned.
     * @throws NoLicenseInstalledException if no license key is installed.
     * @throws Exception                   for any other reason.
     *                                     Note that you should always use
     *                                     {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                                     localized) meaningful detail message.
     * @see #validate(LicenseContent)
     */
    protected synchronized LicenseContent verify(final LicenseNotary notary) throws Exception {
        GenericCertificate certificate = getCertificate();
        if (null != certificate)
            return (LicenseContent) certificate.getContent();

        // Load license key from preferences, 
        final byte[] key = getLicenseKey();
        if (null == key)
            throw new NoLicenseInstalledException(getLicenseParam().getSubject());
        certificate = getPrivacyGuard().key2cert(key);
        notary.verify(certificate);
        final LicenseContent content = (LicenseContent) certificate.getContent();
        validate(content);
        setCertificate(certificate);

        return content;
    }

    /**
     * Decrypts, decompresses, decodes and verifies the given license key,
     * validates its license content and returns it.
     *
     * @param key the license key
     *            - may <em>not</em> be {@code null}.
     * @return A clone of the verified and validated content of the license key
     * - {@code null} is never returned.
     * @throws Exception an instance of a subclass of this class for various
     *                   reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #validate(LicenseContent)
     */
    public final synchronized LicenseContent verify(byte[] key) throws Exception {
        return verify(key, getLicenseNotary());
    }

    /**
     * Decrypts, decompresses, decodes and verifies the given license key,
     * validates its license content and returns it.
     *
     * @param key    the license key
     *               - may <em>not</em> be {@code null}.
     * @param notary the license notary used to verify the license key
     *               - may <em>not</em> be {@code null}.
     * @return A clone of the verified and validated content of the license key
     * - {@code null} is never returned.
     * @throws Exception for various reasons.
     *                   Note that you should always use
     *                   {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                   localized) meaningful detail message.
     * @see #validate(LicenseContent)
     */
    protected synchronized LicenseContent verify(final byte[] key, final LicenseNotary notary) throws Exception {
        final GenericCertificate certificate = getPrivacyGuard().key2cert(key);
        notary.verify(certificate);
        final LicenseContent content = (LicenseContent) certificate.getContent();
        validate(content);

        return content;
    }

    /**
     * Uninstalls the current license key.
     *
     */
    public synchronized void uninstall() {
        setLicenseKey(null);
        setCertificate(null);
    }

    /**
     * Initializes the given license {@code content} with some reasonable
     * defaults unless the respective properties have already been set.
     *
     * @see #validate(LicenseContent)
     */
    protected synchronized void initialize(final LicenseContent content) {
        if (null == content.getHolder()) {
            content.setHolder(new X500Principal(CN_USER));
        }
        if (null == content.getSubject()) {
            content.setSubject(getLicenseParam().getSubject());
        }
        if (null == content.getConsumerType()) {
            final Preferences prefs = getLicenseParam().getPreferences();
            if (null != prefs) {
                if (prefs.isUserNode()) content.setConsumerType(USER);
                else content.setConsumerType(SYSTEM);
                content.setConsumerAmount(1);
            }
        }
        if (null == content.getIssuer()) {
            content.setIssuer(new X500Principal(CN + getLicenseParam().getSubject()));
        }
        if (null == content.getIssued()) {
            content.setIssued(new Date());
        }
        if (null == content.getNotBefore()) {
            content.setNotBefore(midnight());
        }
    }

    /**
     * Validates the license content.
     * This method is called whenever a license certificate is created,
     * installed or verified.
     * <p>
     * Validation consists of the following plausability checks for the
     * properties of this class:
     * <p>
     * <ul>
     * <li>'subject' must match the subject required by the application
     *     via the {@link LicenseParam} interface.
     * <li>'holder', 'issuer' and 'issued' must be provided (i.e. not
     *     {@code null}).
     * <li>If 'notBefore' or 'notAfter' are provided, the current date and
     *     time must match their restrictions.
     * <li>'consumerType' must be provided and 'consumerAmount' must be
     *     positive.
     *     If a user preference node is provided in the license parameters,
     *     'consumerType' must also match {@code "User"} (whereby case is
     *     ignored) and 'consumerAmount' must equal 1.
     * </ul>
     * <p>
     * If you need more or less rigid restrictions, you should override this
     * method in a subclass.
     *
     * @param content the license content
     *                - may <em>not</em> be {@code null}.
     * @throws NullPointerException    if {@code content} is {@code null}.
     * @throws LicenseContentException if any validation test fails.
     *                                 Note that you should always use
     *                                 {@link Throwable#getLocalizedMessage()} to get a (possibly
     *                                 localized) meaningful detail message.
     * @see #initialize(LicenseContent)
     */
    protected synchronized void validate(final LicenseContent content) throws LicenseContentException {
        final LicenseParam param = getLicenseParam();
        if (!param.getSubject().equals(content.getSubject())) {
            throw new LicenseContentException(EXC_INVALID_SUBJECT);
        }
        if (null == content.getHolder()) {
            throw new LicenseContentException(EXC_HOLDER_IS_NULL);
        }
        if (null == content.getIssuer()) {
            throw new LicenseContentException(EXC_ISSUER_IS_NULL);
        }
        if (null == content.getIssued()) {
            throw new LicenseContentException(EXC_ISSUED_IS_NULL);
        }
        final Date now = new Date();
        final Date notBefore = content.getNotBefore();
        if (null != notBefore && now.before(notBefore)) {
            throw new LicenseContentException(EXC_LICENSE_IS_NOT_YET_VALID);
        }
        final Date notAfter = content.getNotAfter();
        if (null != notAfter && now.after(notAfter)) {
            throw new LicenseContentException(EXC_LICENSE_HAS_EXPIRED);
        }
        final String consumerType = content.getConsumerType();
        if (null == consumerType) {
            throw new LicenseContentException(EXC_CONSUMER_TYPE_IS_NULL);
        }
        final Preferences prefs = param.getPreferences();
        if (null != prefs && prefs.isUserNode()) {
            if (!USER.equalsIgnoreCase(consumerType))
                throw new LicenseContentException(EXC_CONSUMER_TYPE_IS_NOT_USER);
            if (1 != content.getConsumerAmount())
                throw new LicenseContentException(EXC_CONSUMER_AMOUNT_IS_NOT_ONE);
        } else {
            if (0 >= content.getConsumerAmount())
                throw new LicenseContentException(EXC_CONSUMER_AMOUNT_IS_NOT_POSITIVE);
        }
    }

    //
    // Methods for license certificates.
    //

    /**
     * Returns the license certificate cached from the
     * last installation/verification of a license key
     * or {@code null} if there wasn't an installation/verification
     * or a timeout has occured.
     */
    protected synchronized GenericCertificate getCertificate() {
        return System.currentTimeMillis() < certificateTimeout ? certificate : null;
    }

    /**
     * Sets the given license certificate as installed or verified.
     *
     * @param certificate the license certificate
     *                    - may be {@code null} to clear.
     */
    protected synchronized void setCertificate(GenericCertificate certificate) {
        setCertificate0(certificate);
    }

    private void setCertificate0(GenericCertificate certificate) {
        certificateTimeout = null != (this.certificate = certificate) ? System.currentTimeMillis() + TIMEOUT : 0;
    }

    //
    // Methods for license keys.
    // Note that in contrast to the methods of the privacy guard,
    // the following methods may have side effects (preferences, file system).
    //

    /**
     * Returns the current license key.
     */
    protected synchronized byte[] getLicenseKey() {
        return getLicenseParam().getPreferences().getByteArray(PREFERENCES_KEY, null);
    }

    /**
     * Installs the given license key as the current license key.
     * If {@code key} is {@code null}, the current license key gets
     * uninstalled (but the cached license certificate is not cleared).
     */
    protected synchronized void setLicenseKey(final byte[] key) {
        final Preferences prefs = getLicenseParam().getPreferences();
        if (null != key) {
            prefs.putByteArray(PREFERENCES_KEY, key);
        } else {
            prefs.remove(PREFERENCES_KEY);
        }
    }

    /**
     * Stores the given license key to the given file.
     *
     * @param key     the license key
     *                - may <em>not</em> be {@code null}.
     * @param keyFile the file to save the license key to
     *                - may <em>not</em> be {@code null}.
     *                This should have a {@code LICENSE_SUFFIX}.
     */
    protected static void storeLicenseKey(final byte[] key, final File keyFile) throws IOException {
        final OutputStream out = new FileOutputStream(keyFile);
        try {
            out.write(key);
        } finally {
            try {
                out.close();
            } catch (IOException weDontCare) {
            }
        }
    }

    /**
     * Loads and returns the first megabyte of content from {@code keyFile}
     * as license key in a newly created byte array.
     *
     * @param keyFile the file holding the license key
     *                - may <em>not</em> be {@code null}.
     */
    protected static byte[] loadLicenseKey(final File keyFile)
            throws IOException {
        // Allow max 1MB size files and let the verifier detect a partial read
        final int size = Math.min((int) keyFile.length(), 1024 * 1024);
        final byte[] b = new byte[size];
        final InputStream in = new FileInputStream(keyFile);
        try {
            // Let the verifier detect a partial read as an error
            in.read(b);
        } finally {
            in.close();
        }
        return b;
    }

    //
    // Various stuff.
    //

    /**
     * Returns a license notary configured to use the keystore parameters
     * contained in the current license parameters
     * - {@code null} is never returned.
     */
    protected synchronized LicenseNotary getLicenseNotary() {
        if (null == notary) {
            notary = new LicenseNotary(getLicenseParam().getKeyStoreParam());
        }
        return notary;
    }

    /**
     * Returns a privacy guard configured to use the cipher parameters
     * contained in the current license parameters
     * - {@code null} is never returned.
     */
    protected synchronized PrivacyGuard getPrivacyGuard() {
        if (null == guard) {
            guard = new PrivacyGuard(getLicenseParam().getCipherParam());
        }
        return guard;
    }

    /**
     * Returns a suitable file filter for the subject of this license manager.
     * On Windows systems, the case of the suffix is ignored when browsing
     * directories.
     *
     * @return A valid {@code FileFilter}.
     */
    public synchronized FileFilter getFileFilter() {
        if (fileFilter != null) {
            return fileFilter;
        }
        final String description = Resources.getString(FILE_FILTER_DESCRIPTION, getLicenseParam().getSubject());
        if (File.separatorChar == '\\') {
            fileFilter = new FileFilter() {
                public boolean accept(File f) {
                    return f.isDirectory() || f.getPath().toLowerCase().endsWith(LICENSE_SUFFIX);
                }
                public String getDescription() {
                    return description + FILE_FILTER_SUFFIX;
                }
            };
        } else {
            fileFilter = new FileFilter() {
                public boolean accept(File f) {
                    return f.isDirectory() || f.getPath().endsWith(LICENSE_SUFFIX);
                }
                public String getDescription() {
                    return description + FILE_FILTER_SUFFIX;
                }
            };
        }
        return fileFilter;
    }
}
